﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using System.Threading;

using Norm;

using Fuse8.DomainFramework.Common.Utilities;
using Fuse8.DomainFramework.Common;
using System.IO;

namespace Fuse8.DomainFramework.MongoDbWrapper
{
    /// <summary>
    /// 
    /// </summary>
    public static class MongoDbEntityBuilder
    {
        private static LRUCacheBag<Type, object> _cacheInstance = new LRUCacheBag<Type, object>();

        public static IMongoDbRepositoryEntity CreateInstance<TEntity>() where TEntity : class, new()
        {
            var forType = typeof(TEntity);

            object generatedInstance;

            if (!_cacheInstance.TryGetValue(forType, out generatedInstance))
            {
                generatedInstance = Activator.CreateInstance(
                    WrapperAssembly.GenerateType(string.Format("{0}MongoDbEntity", forType.Name), forType), Type.EmptyTypes);

                _cacheInstance.Add(forType, generatedInstance);
            }

            return (generatedInstance as IMongoDbRepositoryEntity).FastClone();
        }
    }

    internal static class WrapperAssembly
    {
        private static AssemblyBuilder _assemblyBuilder;
        private static ModuleBuilder _moduleBuilder;

        private static readonly TypeAttributes _defaultTypeAttributes =
            TypeAttributes.NotPublic |
            TypeAttributes.AutoClass |
            TypeAttributes.AnsiClass |
            TypeAttributes.Serializable |
            TypeAttributes.BeforeFieldInit;

        static WrapperAssembly()
        {
            AssemblyName assemblyName = new AssemblyName("Fuse8.DomainFramework.Dynamic.MongoDb");

            _assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave);

            _moduleBuilder = _assemblyBuilder.DefineDynamicModule(_assemblyBuilder.GetName().Name, false);   
        }
        
        public static Type GenerateType(string name, Type parent)
        {
            return GenerateType(name, parent, _defaultTypeAttributes);
        }

        public static Type GenerateType(string name, Type parent, TypeAttributes attributes)
        {
            Guard.ArgumentNotEmpty(name, "name");
            Guard.ArgumentNotNull(parent, "parent");

            var interfaceType = typeof(IMongoDbRepositoryEntity);

            return DefineType(name, parent, attributes)
                .CreateInterfaceImplementation(interfaceType)
                .CreateConstructor(parent)
                .CreateKeyProperties(interfaceType)
                .CreateType();
        }

        public static TypeBuilder DefineType(string name, Type parent)
        {
            return DefineType(name, parent, _defaultTypeAttributes);
        }

        public static TypeBuilder DefineType(string name, Type parent, TypeAttributes attributes)
        {
            return _moduleBuilder.DefineType(
                string.Format("{0}{1}", name, Guid.NewGuid().ToString().Replace("-", "")), 
                attributes, 
                parent, 
                Type.EmptyTypes);
        }

        public static void SaveGeneratedAssembly()
        {
            _assemblyBuilder.Save(string.Format("{0}.dll", _assemblyBuilder.GetName().Name));
        }
    }

    internal static class TypeExtensions
    {
        public static TypeBuilder CreateConstructor(this TypeBuilder typeBuilder, Type parent)
        {
            ConstructorBuilder constructor = typeBuilder.DefineConstructor(
                        MethodAttributes.Public |
                        MethodAttributes.HideBySig |
                        MethodAttributes.SpecialName |
                        MethodAttributes.RTSpecialName,
                        CallingConventions.Standard,
                        Type.EmptyTypes);

            ConstructorInfo conObj = parent.GetConstructor(Type.EmptyTypes);

            ILGenerator il = constructor.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Call, conObj);
            il.Emit(OpCodes.Ret);

            return typeBuilder;
        }

        public static TypeBuilder CreateKeyProperties(this TypeBuilder typeBuilder, Type interfaceType)
        {
            return typeBuilder.CreateProperty("MongoDbId", interfaceType);
        }

        public static TypeBuilder CreateInterfaceImplementation(this TypeBuilder typeBuilder, Type interfaceType)
        {
            typeBuilder.AddInterfaceImplementation(interfaceType);            
            
            return typeBuilder;
        }

        private static TypeBuilder CreateProperty(this TypeBuilder typeBuilder, string propertyName, Type interfaceType)
        {
            PropertyInfo interfacedProperty = interfaceType.GetProperty(propertyName);

            MethodAttributes getSetAttr =
                MethodAttributes.Public |
                MethodAttributes.HideBySig |
                MethodAttributes.NewSlot |
                MethodAttributes.SpecialName |
                MethodAttributes.Virtual |
                MethodAttributes.Final;

            FieldBuilder fieldBuilder = typeBuilder
                .DefineField(string.Format("_{0}{1}", propertyName[0].ToString().ToLowerInvariant(), propertyName.Remove(0, 1)),
                    typeof(ObjectId),
                    FieldAttributes.Private);

            PropertyBuilder propertyBuilder = typeBuilder
                .DefineProperty(propertyName,
                    PropertyAttributes.HasDefault,
                    typeof(ObjectId),
                    null);

            MethodBuilder propertyGetMethodBuilder = typeBuilder
                .DefineMethod(string.Format("get_{0}", propertyName),
                    getSetAttr,
                    typeof(ObjectId),
                    Type.EmptyTypes);
            typeBuilder.DefineMethodOverride(propertyGetMethodBuilder, interfacedProperty.GetGetMethod());

            ILGenerator propertyGetIL = propertyGetMethodBuilder.GetILGenerator();

            propertyGetIL.Emit(OpCodes.Nop);
            propertyGetIL.Emit(OpCodes.Ldarg_0);
            propertyGetIL.Emit(OpCodes.Ldfld, fieldBuilder);            
            propertyGetIL.Emit(OpCodes.Ret);

            propertyBuilder.SetGetMethod(propertyGetMethodBuilder);

            MethodBuilder propertySetMethodBuilder = typeBuilder
                .DefineMethod(string.Format("set_{0}", propertyName),
                    getSetAttr,
                    null,
                    new Type[] { typeof(ObjectId) });
            typeBuilder.DefineMethodOverride(propertySetMethodBuilder, interfacedProperty.GetSetMethod());

            ILGenerator propertySetIL = propertySetMethodBuilder.GetILGenerator();

            propertyGetIL.Emit(OpCodes.Nop);
            propertySetIL.Emit(OpCodes.Ldarg_0);
            propertySetIL.Emit(OpCodes.Ldarg_1);
            propertySetIL.Emit(OpCodes.Stfld, fieldBuilder);
            propertySetIL.Emit(OpCodes.Ret);

            propertyBuilder.SetSetMethod(propertySetMethodBuilder);

            return typeBuilder;
        }
    }
}
